﻿// Based on http://www.jebishop.com/2009/11/18/implementing-a-contextmenu-in-silverlight-4-beta/
// Updated by Anvaka
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Windows.Controls.Primitives;
using System.Windows.Markup;

namespace ContextMenuControls
{
  [ContentProperty("Items")]
  public class ContextMenu : ToolTip
  {
    #region ContextMenu attached property
    public static readonly DependencyProperty ContextMenuProperty = DependencyProperty.RegisterAttached("ContextMenu", 
      typeof(ContextMenu),
      typeof(ContextMenu), 
      new PropertyMetadata(null, OnContextMenuChanged));

    public static ContextMenu GetContextMenu(DependencyObject obj)
    {
      return (ContextMenu)obj.GetValue(ContextMenuProperty);
    }
    public static void SetContextMenu(DependencyObject obj, ContextMenu value)
    {
      obj.SetValue(ContextMenuProperty, value);
    }

    private static void OnContextMenuChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
      var element = obj as FrameworkElement;

      if (element == null)
        return;

      if (e.OldValue != null)
        Detach(element, (ContextMenu)e.OldValue);

      if (e.NewValue != null)
        Attach(element, (ContextMenu)e.NewValue);
    }

    private static void Attach(FrameworkElement element, ContextMenu contextMenu)
    {
      contextMenu.PlacementTarget = element;
      contextMenu.Placement = PlacementMode.Mouse;

      if (element != null)
      {
        element.MouseRightButtonDown += ElementMouseRightButtonDown;
        element.MouseRightButtonUp += ElementMouseRightButtonUp;
      }
    }

    private static void Detach(FrameworkElement element, ContextMenu contextMenu)
    {
      contextMenu.PlacementTarget = null;

      if (element != null)
      {
        element.MouseRightButtonDown -= ElementMouseRightButtonDown;
        element.MouseRightButtonUp -= ElementMouseRightButtonUp;
      }
    }
    #endregion

    private readonly ObservableCollection<MenuItemBase> _items;
    private ItemsControl _itemContainerList;

    public ContextMenu()
    {
      DefaultStyleKey = typeof(ContextMenu);

      Opened += OnOpened;
      Closed += OnClosed;
      Loaded += OnLoaded;

      AddHandler(KeyDownEvent, new KeyEventHandler(OnKeyDown), true);

      _items = new ObservableCollection<MenuItemBase>();
      _items.CollectionChanged += ItemsCollectionChanged;
    }

    public override void OnApplyTemplate()
    {
      base.OnApplyTemplate();

      _itemContainerList = GetTemplateChild("ItemList") as ItemsControl;
      if (_itemContainerList != null)
      {
        _itemContainerList.ItemsSource = _items;
      }
    }


    private static void OnLoaded(object sender, RoutedEventArgs e)
    {
      ((ContextMenu)sender).IsHitTestVisible = true;
    }


    private void OnOpened(object sender, RoutedEventArgs e)
    {
      if (Application.Current.RootVisual != null)
      {
        Application.Current.RootVisual.AddHandler(MouseLeftButtonDownEvent, new MouseButtonEventHandler(OnMouseDownElsewhere), true);
        Application.Current.RootVisual.AddHandler(KeyDownEvent, new KeyEventHandler(OnKeyDown), true);
        Application.Current.RootVisual.MouseRightButtonDown += OnMouseDownElsewhere;

        Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));

        ContextMenuManager.CurrentMenu = this;

        Focus();
      }
    }

    private void OnClosed(object sender, RoutedEventArgs e)
    {
      if (Application.Current.RootVisual != null)
      {
        Application.Current.RootVisual.RemoveHandler(MouseLeftButtonDownEvent, new MouseButtonEventHandler(OnMouseDownElsewhere));
        Application.Current.RootVisual.RemoveHandler(KeyDownEvent, new KeyEventHandler(OnKeyDown));
        Application.Current.RootVisual.MouseRightButtonDown -= OnMouseDownElsewhere;
      }
    }



    public ObservableCollection<MenuItemBase> Items
    {
      get { return _items; }
    }

    private void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
      // since we can't make a custom routed event, manually attach and detach event handlers for selection event
      if (e.NewItems != null)
      {
        foreach (object o in e.NewItems)
        {
          var newItem = o as MenuItem;
          if (newItem != null)
          {
            newItem.Click += MenuItem_Selected;
          }
        }
      }

      if (e.OldItems != null)
      {
        foreach (object o in e.OldItems)
        {
          var oldItem = o as MenuItem;
          if (oldItem != null)
          {
            oldItem.Click -= MenuItem_Selected;
          }
        }
      }
    }

    private void MenuItem_Selected(object sender, EventArgs e)
    {
      var item = sender as MenuItem;

      if (item == null)
        return;

      IsOpen = false;

      if (item.Command != null)
        item.Command.Execute(item.CommandParameter);
    }

    private static void ElementMouseRightButtonDown(object sender, MouseButtonEventArgs e)
    {
      e.Handled = true;
    }

    private static void ElementMouseRightButtonUp(object sender, MouseButtonEventArgs e)
    {
      var element = sender as FrameworkElement;

      if (element != null)
      {
        var menu = element.GetValue(ContextMenuProperty) as ContextMenu;

        // if already open, animate closed and reopen at new position per standard ux
        if (menu != null)
        {
          if (menu.IsOpen)
          {
            menu.IsOpen = false;
          }

          menu.IsOpen = true;
        }
      }
    }



    private void OnKeyDown(object sender, KeyEventArgs e)
    {
      switch (e.Key)
      {
        case Key.Escape:
          // close menu
          IsOpen = false;
          break;
        case Key.Up:
          // select next item in up direction
          ChangeFocusedItem(false);
          break;
        case Key.Down:
          // select next item in down direction
          ChangeFocusedItem(true);
          break;
      }
    }

    protected override void OnMouseRightButtonDown(MouseButtonEventArgs e)
    {
      e.Handled = true;
    }

    private void OnMouseDownElsewhere(object sender, MouseButtonEventArgs e)
    {
      IsOpen = false;
    }



    private void ChangeFocusedItem(bool ascendIndex)
    {
      Type focusedElementType = FocusManager.GetFocusedElement().GetType();
      int startIndex;

      if (focusedElementType == _itemContainerList.GetType() && _items.Count > 0)
      {
        // focused item is the item container list, so try to set focus to an initial item
        startIndex = GetNextItemIndex(-1, ascendIndex);
      }
      else if (focusedElementType != typeof(MenuItem))
      {
        // focused item isn't the item container list or a menu item, so ignore
        return;
      }
      else
      {
        // focused item is already a menu item, so try to set focus to next
        var item = FocusManager.GetFocusedElement() as MenuItem;
        startIndex = GetNextItemIndex(_items.IndexOf(item), ascendIndex);
      }

      int index = startIndex;

      MenuItemBase nextItem = _items[index];

      // focus next item, if it's a menu item
      if (nextItem.GetType() == typeof(MenuItem))
      {
        nextItem.Focus();
      }
      else
      {
        // next element wasn't a MenuItem, so loop through once trying to find the next item
        index = GetNextItemIndex(index, ascendIndex);
        nextItem = _items[index];
        while (nextItem.GetType() != typeof(MenuItem) && index != startIndex)
        {
          index = GetNextItemIndex(index, ascendIndex);
          nextItem = _items[index];
        }
        if (nextItem.GetType() == typeof(MenuItem))
        {
          nextItem.Focus();
        }
      }
    }

    private int GetNextItemIndex(int startIndex, bool ascending)
    {
      int index = startIndex + (ascending ? 1 : -1);

      if (index >= _items.Count)
      {
        // if after end of collection, go to start
        index = 0;
      }
      else if (index < 0)
      {
        // if before start of collection, go to end
        index = _items.Count - 1;
      }

      return index;
    }

  }
}
